﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VirtualCity;

namespace StudentsAssignment
{
  using Casanova.Math;
  using Action = PlanDescription.ActionDescription;
  using City = VirtualCity.PlanDescription.CityExpectations;
  using Agent = VirtualCity.CityDescription.AgentDescription;
  using Building = VirtualCity.CityDescription.BuildingDescription;
  using ItemDescription = VirtualCity.CityDescription.ItemDescription;
  using UseEffects = VirtualCity.CityDescription.UseEffects;
  using WalkGraph = VirtualCity.CityDescription.WalkGraph;
    using System.Threading;
    using System.Diagnostics;

    public class MovesSelector
    {
        static public IEnumerable<Action> SelectMoves(City city, Agent agent)
        {
            Building home = agent.Home;
            Building work = agent.Work;
            WalkGraph walk_graph = city.WalkGraph;

        //    city.
       //     agent.Visual = null;



            // Walk home (this is not needed, since the agent already starts at his home): 
            // yield return Action.NewWalk(home.Position);

            PathWorld path_world = new PathWorld();
            path_world.CreateWorld(walk_graph);

            GoalPlanner planner = new GoalPlanner( agent, city );
            List<List<PlannerNode>> plans = planner.Execute(agent, path_world);

            // no plan found
            if (plans.Count == 0)
            {
                yield break;
            }

            
            List<PlannerNode> selected_plan = plans[0];
            float selected_plan_utility = 0;

            foreach (List<PlannerNode> plan in plans)
            {
                // todo: decide which plan to follow
                PlannerWorldState current_state = PlannerWorldState.GenerateCurrentState(agent, city);
                float start_money = current_state.money;

                // now run through all plan actions to generat the world state after all actions have been executed
                for (int i = 0; i < plan.Count; ++i)
                {
                    current_state = PlannerWorldState.UpdateStateWithAction(new PlannerNode(null, null), current_state, plan[i].action);
                }

                // now check if this state is better than the last state
                // the planner doesn't clamp the values but for calculating the utility of the plan
                // we need to clamp
                float plan_utility = CalcWorldStateUtility(current_state, start_money);

                if (plan_utility > selected_plan_utility)
                {
                    selected_plan_utility = plan_utility;
                    selected_plan = plan;
                }
            }

            Vector2 agent_pos = agent.Position;
            int object_id = -1;

            // now follow the plan
            for (int i = 0; i < selected_plan.Count; ++i)
            {
                PlannerNode node = selected_plan[i];
                PlannerAction action = node.action;

                if (action.type == PlannerActionType.GoTo)
                {
                    // check where we want to go to
                    PlannerLocationType target = ReadGotoTarget(action);

                    Vector2 target_pos = agent.Home.Position;

                    if (target == PlannerLocationType.Home) {
                        target_pos = agent.Home.Position;
                    }
                    else if (target == PlannerLocationType.Store) {
                        target_pos = FindBestStore(agent, city, ref object_id );
                    }
                    else if (target == PlannerLocationType.Work)
                    {
                        target_pos = agent.Work.Position;
                    }
                    else if (target == PlannerLocationType.Park)
                    {
                        target_pos = FindBestPark( agent, city, ref object_id );
                    }
                    else if (target == PlannerLocationType.Cinema)
                    {
                        // check if a goto work is contained in the plan
                        // if this is the case there is not enough time to actually watch the movie
                        // so stop the path now
                        bool found_work_slot = false;
                        for (int k = 0; k < selected_plan.Count; ++k)
                        {
                            if (selected_plan[k].action.type == PlannerActionType.Work)
                            {
                                i = selected_plan.Count;
                                found_work_slot = true;
                                break;
                            }
                        }
                        if (found_work_slot == false)
                        {
                            target_pos = FindBestCinema(agent, city, ref object_id);
                        }
                        else
                        {
                            break;
                        }

                    }
                    else
                    {
                        Debug.Assert(false, "current target not yet supported");
                    }

                    // calculate path and follow it
                    List<PathNode> path = new List<PathNode>();
                    int path_lengh = 0;
                    path_world.FindPath(agent_pos.X, agent_pos.Y, target_pos.X, target_pos.Y, ref path, 1000000, ref path_lengh);

                    // Walk to work (will fail, because home and work are not connected; should check walk_graph)
                    // yield return Action.NewWalk(work.Position);
                    // now try to folow the path
                    for (int n = 1; n < path.Count; ++n)
                    {
                        yield return Action.NewWalk(path[n].pos);
                    }

                    agent_pos = target_pos;
                }
                else if (action.type == PlannerActionType.Buy)
                {
                    var store = city.Buildings[object_id];
                    ItemDescription item = null;
                    int effect_id = -1;

                    for (int item_id = 0; item_id < city.Buildings[object_id].Item1.Length; ++item_id)
                    {
                        if (city.Buildings[object_id].Item1[item_id].Name == "Groceries")
                        {
                            // found the correct 
                            effect_id = 0;
                            item = city.Buildings[object_id].Item1[item_id];
                            break;
                        }
                    }

                    yield return Action.NewUse(agent_pos, item, effect_id);
                }
                else if (action.type == PlannerActionType.HaveSnack)
                {
                    int home_id = FindHomeInCity(home, city);
                    int effect_id = -1;
                    ItemDescription item = null;

                    for (int item_id = 0; item_id < city.Buildings[home_id].Item1.Length; ++item_id)
                    {
                        if (city.Buildings[home_id].Item1[item_id].Name == "Kitchen")
                        {
                            // found the correct 
                            effect_id = 1;
                            item = city.Buildings[home_id].Item1[item_id];
                            break;
                        }
                    }

                    yield return Action.NewUse(city.Buildings[home_id].Item2.Position, item, effect_id);
                }
                else if (action.type == PlannerActionType.Work)
                {
                    int work_id = FindInCity(work.Position, city);

                    for (int item_id = 0; item_id < city.Buildings[work_id].Item1.Length; ++item_id)
                    {
                        for (int effect_id = 0; effect_id < city.Buildings[work_id].Item1[item_id].UseEffects.Length; ++effect_id)
                        {
                            if (city.Buildings[work_id].Item1[item_id].UseEffects[effect_id].MoneyGained > 0)
                            {
                                yield return Action.NewUse(city.Buildings[work_id].Item2.Position, city.Buildings[work_id].Item1[item_id], effect_id);

                                item_id = 5000;
                                break;
                            }
                        }
                    }
                }
                else if (action.type == PlannerActionType.Sleep)
                {
                    int home_id = FindHomeInCity(home, city);
                    int item_id = FindItemByName(city, home_id, "Sofa");
                    int effect_id = FindEffectByName(city, home_id, item_id, "Sleep");

                    yield return Action.NewUse(
                        city.Buildings[home_id].Item2.Position,
                        city.Buildings[home_id].Item1[item_id],
                        effect_id);
                }
                else if (action.type == PlannerActionType.WatchTV)
                {
                    int home_id = FindHomeInCity(home, city);
                    int item_id = FindItemByName(city, home_id, "Television");
                    int effect_id = FindEffectByName(city, home_id, item_id, "Watch TV");

                    yield return Action.NewUse(
                        city.Buildings[home_id].Item2.Position,
                        city.Buildings[home_id].Item1[item_id],
                        effect_id);
                }
                else if (action.type == PlannerActionType.Walk)
                {
                    // int home_id = FindHomeInCity(home, city);
                    int home_id = object_id;
                    int item_id = FindItemByName(city, home_id, "Woods trail");
                    int effect_id = FindEffectByName(city, home_id, item_id, "Walk");

                    yield return Action.NewUse(
                        city.Buildings[home_id].Item2.Position,
                        city.Buildings[home_id].Item1[item_id],
                        effect_id);
                }
                else if (action.type == PlannerActionType.CookAndEat)
                {
                    // int home_id = FindHomeInCity(home, city);
                    int home_id = FindHomeInCity( home, city);
                    int item_id = FindItemByName(city, home_id, "Kitchen");
                    int effect_id = FindEffectByName(city, home_id, item_id, "Cook and eat");

                    yield return Action.NewUse(
                        city.Buildings[home_id].Item2.Position,
                        city.Buildings[home_id].Item1[item_id],
                        effect_id);
                }
                else if (action.type == PlannerActionType.WatchMovie)
                {
                    // int home_id = FindHomeInCity(home, city);
                    int home_id = object_id;
                    int item_id = FindItemByName(city, home_id, "Cinema-screen");
                    int effect_id = FindEffectByName(city, home_id, item_id, "Watch movie");

                    yield return Action.NewUse(
                        city.Buildings[home_id].Item2.Position,
                        city.Buildings[home_id].Item1[item_id],
                        effect_id);
                }
                else
                {
                    Debug.Assert(false, "current action not yet supported");
                }
            }
        }

        private static Vector2 FindBestCinema(Agent agent, City city, ref int object_id)
        {
            float distance = 0xFFFFFFF;
            object_id = -1;

            for (int i = 0; i < city.Buildings.Length; ++i)
            {
                var items = city.Buildings[i].Item1;

                for (int j = 0; j < items.Length; ++j)
                {
                    if (items[j].Name == "Cinema-screen" && items[j].UseEffects.Length > 0)
                    {
                        float dist_to_building = (city.Buildings[i].Item2.Position - agent.Home.Position).Length;

                        if (dist_to_building < distance)
                        {
                            object_id = i;
                            distance = dist_to_building;
                        }
                    }
                }
            }

            Debug.Assert(object_id > -1, "couldn't find a cinema");

            return city.Buildings[object_id].Item2.Position;
        }

        private static float CalcWorldStateUtility(PlannerWorldState current_state, float start_money )
        {
            float utility = 0.0f;

            utility += ClampFloat(current_state.enjoyed);
            utility += ClampFloat(current_state.fit);
            utility += ClampFloat((float)current_state.groceries / 10.0f);
            utility += (current_state.money - start_money) / (start_money);
            utility += ClampFloat(current_state.relaxed);
            utility += ClampFloat(current_state.rested);
            utility += ClampFloat(current_state.sated);

            return utility;
        }

        private static float ClampFloat(float p)
        {
            if (p > 1.0f) return 1.0f;

            return p;
        }

        private static Vector2 FindBestPark(Agent agent, City city, ref int object_id)
        {
            float distance = 0xFFFFFFF;
            object_id = -1;

            for (int i = 0; i < city.Buildings.Length; ++i)
            {
                var items = city.Buildings[i].Item1;

                for (int j = 0; j < items.Length; ++j)
                {
                    if (items[j].Name == "Woods trail" && items[j].UseEffects.Length > 0)
                    {
                        float dist_to_building = (city.Buildings[i].Item2.Position - agent.Home.Position).Length;

                        if (dist_to_building < distance)
                        {
                            object_id = i;
                            distance = dist_to_building;
                        }
                    }
                }
            }

            Debug.Assert(object_id > -1, "couldn't find a park");

            return city.Buildings[object_id].Item2.Position;
        }

        private static int FindEffectByName(City city, int building_id, int item_id, string p)
        {
            var building = city.Buildings[building_id];
            var item = building.Item1[item_id];

            for (int i = 0; i < item.UseEffects.Length; ++i)
            {
                if (item.UseEffects[i].Description == p)
                {
                    return i;
                }
            }

            Debug.Assert(false, "effect with the given name wasn't found");
            return -1;
        }
        public static int TryFindItemByName(City city, int building_id, string p)
        {
            var building = city.Buildings[building_id];

            for (int i = 0; i < building.Item1.Length; ++i)
            {
                if (building.Item1[i].Name == p)
                {
                    return i;
                }
            }

            return -1;
        }


        public static int FindItemByName(City city, int building_id, string p )
        {
            int id = TryFindItemByName(city, building_id, p);

            Debug.Assert( id >= 0 , "item with the given name wasn't found");
            return id;
        }

        public static int FindInCity(Vector2 vector2, City city)
        {
            for (int i = 0; i < city.Buildings.Length; ++i)
            {
                if ((vector2 - city.Buildings[i].Item2.Position).Length < 1.0f)
                {
                    return i;
                }
            }

            return -1;
        }

        public static int FindHomeInCity(Building buildingDescription, City city)
        {
            return FindInCity(buildingDescription.Position, city);
        }

        private static Vector2 FindBestStore(Agent agent, City city, ref int store_id)
        {
            float distance = 0xFFFFFFF;
            store_id = -1;

            // todo: add calculations to actually decide which store is best
            // for now just pick the first one found
            for (int i = 0; i < city.Buildings.Length; ++i)
            {
                var items = city.Buildings[i].Item1;

                for (int j = 0; j < items.Length; ++j)
                {
                    if (items[j].Name == "Groceries" && items[j].UseEffects.Length > 0 )
                    {
                        float dist_to_building = (city.Buildings[i].Item2.Position - agent.Home.Position).Length;

                        if (dist_to_building < distance)
                        {
                            distance = dist_to_building;
                            store_id = i;
                        }
                        else
                        {
                            continue;
                        }
                    }
                }
            }

            Debug.Assert(store_id > -1, "no store found");

            return city.Buildings[store_id].Item2.Position;
        }

        private static PlannerLocationType ReadGotoTarget(PlannerAction action)
        {
            for (int i = 0; i < action.effects.Count; ++i)
            {
                if (action.effects[i].type == PlannerActionEffectType.LocationChange)
                {
                    return action.effects[i].location_target;
                }
            }

            Debug.Assert(false, "no location changed effect was specified for the provided action");

            return PlannerLocationType.Home;
        }
    }
}